home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
LSD Docs
/
LSD Docs.iso
/
FILEZ
/
lsd15.dms
/
lsd15.adf
/
Ctutorial2.doc.pp
/
Ctutorial2.doc
Wrap
Text File
|
1991-02-19
|
31KB
|
613 lines
ANOTHER C TUTURIAL (WHERE DO THEY ALL COME FROM?)
By Joseph M. Hinkle.
This is raw text prepared for articles and a book on programming the
Amiga for novices with the intent of quickly getting the reader into the
more powerful features fairly quickly. There are some errors of
explanation of more abstruse things which I need to clean up, but in the
main() (Ha! a little c joke) you will find this useful. Because it is a
kind of super-outline of the book it moves along at a pace you may find
breathtaking if you are new to programming the machine, but careful study
of this and the ROM Kernel Manuals will help you greatly.
The tutorial explains lists, tasks, and startup code with comments on
programming style and example programs which I have tested thoroughly. The
discussion and examples are valid for Workbench v1.2 (v1.3 is just becoming
available as I post this, so I haven't seen anything on it yet, but I doubt
there would be much trouble using that version). I use the Lattice v4.01
compiler and don't have Manx, so can't say much about compiling with that
one. Expect some minor trouble, but the examples are Amiga specific, not
compiler specific, for the most part.
I hang around A-Link in Everett, Washington at (206) 774-4735, run by
the charming and gracious John (He paid me to say this) Willott, and I go
by the name Marty Hinkle on the board. Please send me questions and
comments there.
I also have a tutorial on making shared libraries yourself without using
assembly code (other than the skeleton in the RKM) which does not require
you to be proficient in assembly coding. If someone is interested in that,
I'll straighten it up and upload it.
This production comes from extensive study of the machine itself with
little input from Commodore (It is probably quite shortsighted of me to not
e in their developer's program). A shareware style donation will not be
inappropriate, and you will get a free copy of the whole book if I can ever
get a publisher.
Joseph M. Hinkle
Route 2, Box 2647
Lopez, Washington 98261
PROGRAMMING THE AMIGA - FROM NOVICE TO ADVANCED
Anyone getting the idea to write a useful program on the Amiga decides
to get the Rom Kernel Manual and DOS manual to learn how the system works.
The size of the manuals gives one pause. Reading them is daunting. After
a bit of study one realizes there is no good overview of the executive and
disk operating system, but there is plenty of detail. Trying out some of
the example programs can lead to odd results as there are many small
mistakes in them which one can overlook. After getting something to work
many questions are left, such as "What will make this program work under
Workbench?", "How can this program be made to multitask?", "What is a
process?", and "How can this program be loaded by another?". All of these
are mentioned in the books and there is much discussion of them, but
nowhere are there clear examples of useful programs that implement these
things. You will find, after some experience with the machine, that nearly
every point is mentioned somewhere in the books. Tying them all together
is the difficult part.
I will cover these questions and more by showing a simple program to
establish a style, then using that program to build ever more involved
programs which will take you through the heart of the Amiga. I will refer
to the RKM (ROM Kernel Manual, Addison-Wesley, in four volumes: ROM Kernel
Reference Manual: Libraries and Devices, ROM Kernel Reference
Manual:Exec, Intuition Reference Manual, and Hardware Reference Manual),
and the AmigaDOS Manual, Bantam Books. These might appear pricey, but for
serious programming on the Amiga they are indispensable. If you are new
to the c programming language you should also have a text on c such as the
original language definition "The c Programming Language" by Kernighan and
Ritchie, Prentice-Hall, or another textbook on the subject. Make sure you
understand data structures because there will be a lot of references to
them. Let's get started:
We will need a timer for this experiment, something that goes tick,
tick, tick, every second or so. There is a section in the RKM: Libraries
and Devices called Timer Device. To see if everything will work as
advertised, let's try a simple delay. We need to open a timer, run it,
and then close it. In order to open a timer we need to fill in a
timerequest structure (RKM:Libraries and Devices, Include Files,
devices/timers.h. Note the .i structures are for assembly language
writers). Note the structure is tagged timerequest, not timeRequest as in
the text. The structure is:
struct timerequest {
struct IORequest tr_node;
struct timeval tr_time;
}
Aha! A typical Amiga data structure! It consists of nothing more than
other structures! Sigh. There is hope, though. The struct timeval is in
the same include file:
struct timeval {
ULONG tv_secs;
ULONG tv_micro;
}
But what is a ULONG? Look in RKM:Exec, Include Files, exec/types.h:
typedef unsigned long ULONG;
Now we know the timerequest structure is a template for an IORequest
structure and two 32 bit quantities. At the top of the file timer.h there
is a line:
#include exec/io.h
So, while you're thumbing through RKM:Exec, look at exec/io.h. You will
find:
struct IORequest {
struct Message io_Message;
struct Device *io_Device;
struct Unit *io_Unit;
UWORD io_Command;
UBYTE io_Flags;
BYTE io_Error;
}
That file #includes exec/ports.h, which defines the struct Message and
struct MsgPort. That file in turn references exec/nodes.h, exec/lists.h
and exec/tasks.h, which contain the definitions needed to completely
specify the struct timerequest (In my compiler's include files there is
no mention of #including exec/types.h, so I have to do that myself.
#including devices/timer.h gets all the rest). The reason for belaboring
all the various data structures is to show you what happens when a request
is made, that is, just what data is passed around the machine and what
Exec does with it. Exec, the executive program, supervises the use of the
microprocessor, responding to interrupts and checking tables of things to
do. It is generally invisible to you but it helps to know it is there,
and we will be using its library of functions often.
This exercise also gets you used to referencing the RKM, which you will
also be doing often. Continuing with the full definition of struct
timerequest, we see it is a template for memory arranged like this,
assuming we call the structure TR:
LONG TR.tr_node.io_Message.mn_Node.ln_Succ ; points to the following node
LONG TR.tr_node.io_Message.mn_Node.ln_Pred ; points to the preceding node
BYTE TR.tr_node.io_Message.mn_Node.ln_Type ; defines the type of node
BYTE TR.tr_node.io_Message.mn_Node.ln_Pri ; this node's priority
LONG TR.tr_node.io_Message.mn_Node.ln_Name ; points to a text string name
LONG TR.tr_node.io_Message.mn_ReplyPort ; points to a message port
WORD TR.tr_node.io_Message.mn_Length ; length of this whole block
LONG TR.tr_node.io_Device ; points to a struct Device
LONG TR.tr_node.io_Unit ; points to a struct Unit
WORD TR.tr_node.io_Command ; a command
BYTE TR.tr_node.io_Flags ; flags set
BYTE TR.tr_node.io_Error ; error returned
LONG TR.tr_time.tv_secs ; seconds requested
LONG TR.tr_time.tv_micro ; microseconds requested
These 40 bytes are the complete timerequest structure. We fill in some
members ourselves and functions provided in the system fill in others. In
particular, we need to fill in what kind of timer it is, how we want the
timer to behave (the command), and when we want the timer to reply to us.
First, however, we have to allocate some memory for the structure.
There is a routine available which will do that for us, CreateExtIO(), but
one of the things it needs is the address of a message port (struct
MsgPort), which, as you recall from looking in exec/ports.h, is a little
structure itself, starting with a Node structure. Are you getting the
idea that everything starts with a Node structure? Just about everything
does, because Exec uses these to link them into Lists (RKM:Exec, Lists)
which are scanned periodically to see if anything needs doing, or when a
routine wants to see if it has any mail. Whole data structures are not
passed around in the machine, just addresses of nodes, which also happen
to be the first addresses of most structures. We will get into detail on
that later; suffice to say that if we ever get this memory allocated we
will ask somebody to link our structure into some list or another so it
will get processed.
There is a good discussion of memory allocation in RKM:Exec, Memory
Allocation but fortunately there is a routine for that, too, for certain
cases such as we need now. CreatePort(), which needs only a name and a
priority (RKM:Exec, Tasks, et al), will provide us with the address of a
MsgPort structure we can use to call CreateExtIO(), which will provide us
with an address of a timerequest structure, which in turn we will use to
open a timer device. For now, we will make everything priority 0.
These routines should be declared as functions returning something to
make life easier as we write the program. Some, like CreatePort(), always
return the address of a MsgPort structure, so we can declare them
globally. Others, like CreateExtIO() return the address of differently
sized blocks, depending on what we are doing, so they should be cast as
the type we need as we call them.
We also have to remember to free up the memory when we are done, and
the routines DeletePort() and DeleteExtIO() will do that for us. After
all this discussion, the problem is again beginning to look simpler. We
will eventually fix things so they will get simpler yet. These routines,
by the way, are described in RKM:Libraries and Devices, Library Summaries.
So here it is:
/***** Tick.c ***********************************************************/
#include "exec/types.h"
#include "devices/timer.h"
struct MsgPort *CreatePort();
void
main()
{
struct MsgPort *TP;
struct timerequest *TR;
int error;
TP = CreatePort(NULL, 0);
if (TP == NULL) {
printf("Not enough memory for the Message Port\n");
exit(0);
}
TR = (struct timerequest *)
CreateExtIO(TP, sizeof(struct timerequest));
if (TR == NULL) {
printf("Not enough memory for the timerequest\n");
DeletePort(TP);
exit(0);
}
error = OpenDevice("timer.device", UNIT_VBLANK, TR, 0);
if (error > 0) {
printf("The timer won't open\n");
DeleteExtIO(TR, sizeof(struct timerequest));
DeletePort(TP);
exit(0);
}
TR->tr_node.io_Command = TR_ADDREQUEST;
TR->tr_time.tv_secs = 5;
TR->tr_time.tv_micro = 0;
DoIO(TR);
printf("Tick\n");
CloseDevice(TR);
DeleteExtIO(TR, sizeof(struct timerequest));
DeletePort(TP);
}
/***********************************************************************/
You did read the section RKM: Timer Device didn't you? So you know
what a UNIT_VBLANK is, and that the constants in capital letters are
defined in the appropriate include files. I also included error checks
in a crude way for good programming practice. Compile this and play with
it a minute or two.
All we have done here is allocate memory for the two structures that we
use, initialized them with our values, requested the system to link them
into the appropriate lists, and waited. Five seconds later, our routine
wakes up, prints a "Tick" message, and exits. If you don't believe that,
change the value of TR->tr_time.tv_secs to about ten or fifteen, recompile
it, and RUN Tick. A new CLI will be created, and for the time you have
called for you will be able to perform a DOS command like Date in your
original CLI. In the time you have specified, the "Tick" message will
appear and the background CLI will end. You've been multitasking! The
trouble is that the manuals don't clearly show how you can accomplish
that from within a program.
You have read the RKM: Exec, Input/Output so you saw the mention of
DoIO() and the other functions for doing IO. In the program we tried, the
function DoIO() was used to make things simple. We built the appropriate
structures and DoIO() requested the timerequest structure to be linked
into a message port somewhere. Did you notice in your earlier perusal of
the exec/ports.h file that the last item in a struct MsgPort is a struct
List? Not a pointer to a structure, but an actual structure. The ln_Pred
and ln_Succ fields of your timerequest structure are changed by Exec to
point to the proper elements of that list. When your time is up Exec
changes the pointers to link the message (timerequest structure) onto the
MsgPort you called for in CreatePort(). DoIO() sees there is mail, sets
any error codes or flags, and exits. We will be doing quite a bit with
this timer program, so it would be better if we had more convenient
routines to handle allocation and deallocation for us, as well as setting
and stopping a timer. So, before we continue, let's build a file of timer
utilities to help us. You might want to modify these somewhat to suit your
specific needs. In particular, SetTimer() does not now set the
microseconds part of the timerequest so only even second periods are
available. If you wanted, you could change the formal variable to a double
and do the math to get microseconds right in the routine.
An important note about compilers: The stack checking routines
various compilers put into the code will not work in task code. Since we
expect to be using these routines within tasks, they and any of the following
routines which will become part of task code must be compiled with stack
checking disabled. There is a further consideration if you are using Lattice
v4.0: When task code is added to the system task list, register A4 is not
preserved, thereby destroying the code's reference to its data sections.
There is a compiler option to cause it to generate register saving code at
the beginning of each function call. It costs only 24 bytes per function,
and is necessary only for functions referenced by AddTask() or
CreateTask() (q.v.). It's unlikely any of these would be, but you never
know. For Lattice 4.0 the compiler command line would be:
lc -v -y TimerUtilities
Carefully check your compiler documentation for any special treatment
required for task code.
/***** TimerUtilities.c *************************************************/
/* */
/* A package of utilities to control timer devices. /*
/*
/* This package contains: /* */
/* */
/* CreateTimer(unit, priority) */
/* returns: Pointer to a struct timerequest or zero if trouble */
/* AbortTimer(timerequest) */
/* returns: Nothing */
/* SetTimer(time) */
/* returns: Nothing */
/* DeleteTimer(timerequest) */
/* returns: Nothing */
/* */
/************************************************************************/
#include "exec/types.h"
#include "devices/timer.h"
extern struct MsgPort * CreatePort();
struct timerequest *
CreateTimer(unit, priority)
ULONG unit;
LONG priority;
{
struct MsgPort *TP;
struct timerequest *TR;
if ((TP = CreatePort(NULL, priority)) == NULL)
return(NULL);
if ((TR = (struct timerequest *)
CreateExtIO(TP, sizeof(struct timerequest))) == NULL) {
DeletePort(TP);
return(NULL);
}
if (OpenDevice(TIMERNAME, unit, TR, 0) != NULL) {
DeleteExtIO(TR, sizeof(struct timerequest));
DeletePort(TP);
return(NULL);
}
return(TR);
}
void
AbortTimer(T)
struct timerequest *T;
{
if (AbortIO(T) == NULL) {
Wait(1 << T->tr_node.io_Message.mn_ReplyPort->mp_SigBit);
GetMsg(T->tr_node.io_Message.mn_ReplyPort);
}
}
void
SetTimer(T, time)
struct timerequest *T;
int time;
{
AbortTimer(T);
T->tr_node.io_Command = TR_ADDREQUEST;
T->tr_time.tv_secs = time;
T->tr_time.tv_micro = 0;
SendIO(T);
}
void
DeleteTimer(TR)
struct timerequest *TR;
{
struct MsgPort *TP;
if (TR != 0) {
AbortTimer(TR);
TP = TR->tr_node.io_Message.mn_ReplyPort;
CloseDevice(TR);
DeleteExtIO(TR, sizeof(struct timerequest));
DeletePort(TP);
}
}
/************************************************************************/
Compile these, but don't link them. There's nothing to link to yet.
Notice the routines don't use DoIO(). They use SendIO() instead. That
will allow us to use them in a task we will create Real Soon Now. First,
let's write a test routine to see if everything works. We'll include an
argument this time so we will be able to set the timer to anything we
want. We'll use the function WaitPort() to wait for completion of the
timer. This gets us closer to a full multitasking program. The reason it
isn't full multitasking is that we aren't doing anything else while
waiting for a message (the timerequest structure) to arrive at our message
port.
/***** Test.c ***********************************************************/
#include "exec/types.h"
#include "devices/timer.h"
struct timerequest *CreateTimer();
void
main(argc, argv)
int argc;
char *argv[];
{
struct timerequest *T;
struct MsgPort *P;
int delay;
if (argc != 2) {
printf("Please provide a delay time\n");
exit(0);
}
delay = atoi(argv[1]);
T = CreateTimer(UNIT_VBLANK, 0);
P = T->tr_node.io_Message.mn_ReplyPort;
SetTimer(T, delay);
WaitPort(P);
printf("Tock\n");
DeleteTimer(T);
}
/************************************************************************/
Compile this and link it with TimerUtilities.o to get a complete
program. Alternatively, you could combine both files together just for
test purposes. Now run the test program, remembering to include a delay
value: Test 3, for example.
Allright! The utility package is quite a help. Now, look carefully at
AbortTimer(). A brief mention should be made here about the function
AbortIO(). It isn't described in some books. If the IO has completed
when AbortIO() is called, it returns a -1 from a timer device (or a
meaningless value from some other devices), and no message is attached to
the message port. If the IO has not completed and is aborted, the
function returns a 0 and the timerequest structure (our message) is
attached to the message port. We then call GetMsg() to remove it from the
port, something unnecessary in this application, but possibly required in
others that use this routine. We will see more of GetMsg() later. The
way AbortTimer() waits for completion is Wait(). This function waits for
a particular bit to be set. While waiting, of course, just as in the case
of WaitPort() and WaitIO(), Exec can be doing other things. To see that
effect, try Running several copies of Test at the same time: Run Test 30
<CR> Run Test 20 <CR> Run Test 10 <CR>. You should see the word "Tock"
printed three times, ten seconds apart.
I said Wait() waits for a particular bit to be set. This bit is
called a signal, and every message port gets one allocated. They have to be
allocated because for every task, like the one we are running when we run
Test, has a longword (32 bits) in a structure available for signalling.
Each bit has to have a unique meaning. Exec takes the lower sixteen for
itself, leaving the upper sixteen available to us. We don't know which
bits have been allocated already, or to what, so we use a routine
AllocSignal() to find out our bit number. The signal must be freed with
FreeSignal() when we don't need it any more. These details have been
taken care of for us by CreatePort() and DeletePort(). The point is that
there is a number in a message port that is the number of a bit (from 0 to
31) that says when this bit is set in a certain location, mail has arrived
at the port. It stays set until you Wait() for it in some fashion or
another ( WaitPort() or WaitIO() ). We can recreate that bit by shifting
a 1 left the number of times equal to the signal number. The signal
number is in a struct MsgPort, element mp_SigBit. Remembering that in our
timerequest structure there is an IORequest structure containing a
Message structure which contains a pointer to a message port, the
reference is mn_ReplyPort->mp_SigBit. Since that Message structure is
part of an IORequest structure we have to say
io_Message.mn_ReplyPort.mp_SigBit. That IORequest in turn is a named part
of a timerequest structure called tr_node. So, we obtain the number,
shift a 1 left that number of times, and Wait() for it to be set, which
yields the mouthful operation:
Wait(1 << T->tr_node.io_Message.mn_ReplyPort->mp_SigBit);
We won't say that very often, but we will be using Wait() on specific
bits.
Let's review what we've done. We allocated and initialized a MsgPort
structure. That sits off to the side as our mailbox. We allocated and
initialized a timerequest structure, among other things putting the
address of our mailbox in it so the system will know where to send the
structure back to when it's done with it, a return address, if you like.
That's our message. We put some stuff in the message (the command and
time values), sent it off to Exec, and waited for mail to arrive at our
mailbox. Because of the particular functions used for waiting, the
microprocessor can go to sleep if Exec isn't doing anything else. We
could have done busy loop waiting, which consists of looking at some value
in the MsgPort to see if it has changed, and if it hasn't, looking again.
That kind of program won't quite choke up the machine, depending on the
priority it has, but it eats up all the spare processor time and can make
some operations very slow. We will demonstrate that with our timer
routines by and by.
We have been operating as a task spawned by the CLI (A process,
actually, but that is a sort of super task run by DOS. More on that
later). We communicated with the rest of the system by sending messages
and looking for signals. You cannot write a function and call it with
values as in ordinary c programming and have it be a separate task. You
must use the message passing system. Exec, whose name we have been
invoking all along, is primarily a program which manipulates lists, lists
of messages being a large part of a busy program. It is properly called
the executive. Every time a clock interrupt occurs, Exec is activated,
and it goes looking for things to do by scanning various lists. You
don't ever call Exec, you just readdress a message. Exec will know about
it soon enough and take suitable action.
Let's examine lists more closely (You have been diving into the RKM
to look things up all along, haven't you? See RKM: Exec, Lists). A list is
a simple little structure of three pointers and a byte defining its type:
LONG lh_Head ; points to the first node in the list
LONG lh_Tail ; is always zero
LONG lh_TailPred ; points to the last node in the list
BYTE lh_Type ; defines the types of nodes in the list
BYTE lh_pad ; is here to make the structure an even number of bytes
The lh_Type field signifies what kind of a list it is. Those
definitions are in the include file exec/nodes.h, and can be tasks,
interrupts, memory, all the things that Exec keeps track of.
If the list is empty the lh_Head element points to its lh_Tail and the
lh_TailPred points to its lh_Head.
---- lh_Head <------
| |
---> lh_Tail = 0 |
|
lh_TailPred ---
Figure 1
Now if a node is added to the list, the list's head is made to point
to the node, that is, the first location of the node. The node has no
successors, so the ln_Succ field is made to point to the list's lh_Tail,
which is always zero so a search routine has a place to stop. The node's
predecessor is the list, so that field is made to point to the first
location of the list. As more nodes are added on to the list, the ln_Succ
field is made to point to the first location of the following node and the
following node's ln_Pred field is made to point to the first location of
the preceding node. That way all nodes are linked forwards and backwards.
A list and the nodes linked into it could be represented like this:
Node:
-------> ln_Succ -------<-|
| ---- ln_Pred | |
List: | | ln_Type | |
lh_Head -------<--- ln_Pri | |
(zero) lh_Tail <-------- ln_Name | |
lh_TailPred --- | | |
| | | |
| | Node: | |
| | ln_Succ <======<-+--|
| | ln_Pred ------+--| |
| | | |
| | | |
| | Node: | |
|_====> ln_Succ <-----| |
ln_Pred ------------|
Figure 2
A node has a type, just like a list, and generally agrees with the
type of list it is in. It also has a priority so that Exec can establish the
order in which it will process a node. There is a name field which can be
a pointer to a character string such as "Initial CLI" to give the
structure an identifier that can be searched for without knowing which
list it is in.
Nodes by themselves aren't of any use, but they are the beginning
structure of almost every structure in the system. Recalling the
discussion of messages and the timerequest structure, you can see how
messages can be sent to a message port just by finding where the
lh_TailPred field of the message port points, putting that address in the
ln_Pred of the message, and changing the contents of that address to point
to the first location of the message. Since it will be the latest message
attached to the port, its ln_Succ field will be filled with the address of
the lh_TailPred field of the message port. Exec has to change just those
three addresses and three more in the list the message came from, if it
was in one, to send it, no matter how large the message. You don't have
to bother yourself with these details as there are routines that perform
these functions, allowing you to think of "sending" or "putting" a message
somewhere and waiting for a "reply". The list structure comes more
readily to mind when we link something into one, such as a task (See RKM:
Exec, Tasks and include/exec/tasks.h).
Tasks are jobs that Exec performs when the conditions warrant, such
as a timer running out, or a key being pressed. There are certain
limitations to tasks as such. They are procedures which cannot be called
by another function (Exec does that when the time is right, and it is best
not to interfere with the opertion of Exec. It bytes). A task cannot
return a result, and as you shall see, should probably not return at all.
A task cannot call any DOS related input or output functions that require
multitasking like printf(), although way down the line I'll give some
pointers on accomplishing those functions.
Communication with a task is done by message passing. There are
standard system messages like we have been using (the timerequest
structure), and you can construct messages to suit your taste as long as
Exec understands them (the beginning structure is a Message structure) and
your task understands them (the remainder of the structure contains
information meaningful to it). There is a special kind of message called
a semaphore, used to provide a means of mutual exclusion, and
communication is also done by signals, which are single bits of an
unsigned longword. One other means not supported by Exec is by global
variables. If your task can see a variable changed by another program,
your task can act on it. However, a very important point for a
multitasking system, you must not busy wait in a task as you will eat up
all the spare time the microprocessor has. Notice I said spare time and
not necessarily all its time. That depends on your task's priority. Busy
waiting is exemplified by this:
s = 0;
while (s != 999999999) {
s = s + 1;
}
Later on we will try a busy waiting routine to see what happens.
Signals are the simplest executive system for intertask communication.
Every task has available an unsigned longword called tc_SigAlloc. I'm
sure you have your RKM:Exec book open to the include/exec/tasks section
this very moment, so have a look at the other signal related elememts
tc_SigWait and tc_SigRecvd. Signals are single bits of the longword
tc_SigAlloc. As I mentioned earlier in the di^C
End.